%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Database of active view_fsm instances. Each view_fsm key is associated
%%% with a bag of op refs with compositional folder ref and cookie.
%%%
%%% We use Mnesia, which handles distribution and transactions.
%%%
%%% It _may_ be faster to serialize writes through a view_db_srv and then
%%% use dirty write/deletes as well as reads.
%%% [http://www.erlang.org/doc/man/mnesia.html#async_dirty-2]
%%%
%%% This is updated in-process by view_fsm instances, who register their
%%% availability to handle op input emissions.
%%%
%%% It is read in-process by fieldset_srv, to determine which view_fsm
%%% instances to send emissions to.
%%%
%%% The server instance is linked to view_fsm instances. This means it receives
%%% 'EXIT' messages from view_fsm instances on on the node, and removes them
%%% from the distributed Mnesia activation table.
%%%
%%% @headerfile "../include/txm.hrl"

-module(view_db).
-compile(export_all).

%% The view DB server is linked to activated view_fsm processes, and
%% cleans their entries from the view db when they die.
-behaviour(gen_server).

-include("txm.hrl").
-include("view.hrl").
-include("struct.hrl").

%% Allow use of ets:fun2ms/1 and qlc:q/1.
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("stdlib/include/qlc.hrl").

%% gen_server callback handles.
-define(LINK_VIEW_FSM, link_view_fsm).
-define(GET_ACTIVE_VIEWS, get_active_views).

%% View FSMs populate the cache when attaching to a folder.
-export([start_link/0, stop/0]).
-export([activate/4, deactivate/1]).
-export([matching_views/3]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3]).
-export([active_views/1, active_views/2]).
-export([terminate/2]).
-export([create_tables/0]).

%% ----------------------------------------------------------------------------
%% In-process client functions.
%% ----------------------------------------------------------------------------

%% @doc Writes entries into the activation table allowing the calling process
%% (a fieldset_srv) to be looked up when emitting input.
activate(ViewFsm, CompRef, Cookie, EmitOps) ->
  ?DEBUG([activate, {view, ViewFsm}, {compref, CompRef}, {cookie, Cookie}]),
  F = fun() ->
           lists:foreach(fun(OpRef) ->
                              mnesia:write(
                                #?ACTIVATION{viewFsm = ViewFsm,
                                             compRef = CompRef,
                                             cookie = Cookie,
                                             opRef = OpRef})
                         end, EmitOps)
      end,
  do_fun(F),
  link_view_fsm(ViewFsm).

%% @doc Deletes all entries in the activation table corresponding to
%% the `ViewFsm' which is primary key.
deactivate(ViewFsm) ->
  F = fun() ->
           mnesia:delete({?ACTIVATION, ViewFsm})
      end,
  do_fun(F).

%% @doc Retrieves all view_fsm active on the supplied compositional
%% folder ref, cookie and op ref. A view will match if its cookie is
%% either '$any_cookie' or a match of the process's cookie.
matching_views(CompRef, Cookie, OpRef) ->

  % fun2ms/1 is a macro that generates a match specification. We can't
  % match the called function head with the fun params, we have to use
  % a when clause.
  MatchSpec =
    ets:fun2ms(
      fun(#?ACTIVATION{viewFsm = ViewFsm,
                       compRef = CompRef1,
                       cookie = Cookie1,
                       opRef = OpRef1})
           when CompRef1 == CompRef andalso
                  (Cookie1 == Cookie
                    orelse Cookie1 == ?ANY_COOKIE) andalso
                  OpRef1 == OpRef ->
           ViewFsm
      end),

  % Dirty read is about 10x quicker than transactional read.
  mnesia:dirty_select(?ACTIVATION, MatchSpec).

%% ----------------------------------------------------------------------------
%% Server process client functions.
%% ----------------------------------------------------------------------------

%% @doc Starts the view_db, linked to the calling process.
start_link() ->
  gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% @doc Stops the view_db.
stop() ->
  gen_server:call(?MODULE, ?STOP).

%% @doc Links the server to the specified view_fsm instance.
link_view_fsm(ViewFsm) ->
  gen_server:call(?MODULE, {?LINK_VIEW_FSM, ViewFsm}, ?SYNC_CALL_TIMEOUT).

%% @doc Returns a struct detailing views and the ops which they are
%% connected to in the activations table, given the supplied composition
%% folder reference.
%% @spec active_views(ref()) -> struct()
active_views(CompRef) ->
  active_views(CompRef, ?ANY_COOKIE).

active_views(CompRef, Cookie) ->
  gen_server:call(?MODULE, {?GET_ACTIVE_VIEWS, CompRef, Cookie},
                  ?SYNC_CALL_TIMEOUT).

%% ----------------------------------------------------------------------------
%% gen_server callbacks.
%% ----------------------------------------------------------------------------

%% @doc Initialization callback.
init(_Args) ->
  ?INFO([init]),
  process_flag(trap_exit, true),
  ?OK_RETURN([]).

%% @doc No special action on termination.
terminate(_Reason, _State) ->
  ?INFO([terminate]).

%% @doc When we receive an exit message from a linked view_fsm, deactivate it.
handle_info({'EXIT', ViewFsm, _Reason}, State) ->
  deactivate(ViewFsm),
  {noreply, State}.

%% @doc Links the server process to the specified view_fsm, so we are
%% notified when the view is killed or terminates.
handle_call({?LINK_VIEW_FSM, ViewFsm}, _From, State) ->
  link(ViewFsm),
  {reply, ?OK_RETURN, State};

%% @doc Returns a struct holding the active views on the specified composition
%% folder reference. Note that a view is normally attached by specifying some
%% sub-folder of the composition folder that actually defines the program. This
%% function will return all such views.
handle_call({?GET_ACTIVE_VIEWS, CompRef, Cookie}, _From, State) ->
  Structs = active_view_structs(CompRef, Cookie),
  {reply, ?OK_RETURN(Structs), State};

%% @doc Stops the server, ok is returned by gen_server to the caller.
handle_call(?STOP, _From, State) ->
  {?STOP, normal, ?OK_RETURN, State}.

%% ----------------------------------------------------------------------------
%% Utility functions.
%% ----------------------------------------------------------------------------

%% @doc Creates the memory-based activation table whose primary key is viewFsm.
%% The type is bag, so that we can have multiple but unique entries per
%% viewFsm key. All fields are indexed to allow rapid lookup of viewFsm
%% by compRef, opRef and cookie at runtime.
create_tables() ->
  ?INFO([create_table, {mnesia, ?ACTIVATION}]),
  mnesia:create_table(
    ?ACTIVATION,
    [{type, bag},
     {ram_copies, [node()]},
     {attributes, record_info(fields, ?ACTIVATION)},
     {index, tl(record_info(fields, ?ACTIVATION))}]).

%% @doc
active_view_structs(CompRef, Cookie) ->

  % Query that returns unique views on the composition folder ref matching
  % the supplied cookie ('$any_cookie' matches all cookies).
  UniqueViewsFn = fun() ->
                       qlc:q([V || #?ACTIVATION{viewFsm = V,
                                                compRef = CR,
                                                cookie = CO} <-
                                      mnesia:table(?ACTIVATION),
                                   CR == CompRef,
                                   (Cookie == ?ANY_COOKIE orelse CO == Cookie)],
                             unique)
                  end,

  % Query that returns all op refs on a given view.
  OpStructsFn = fun(ViewFsm) ->
                     qlc:q([{?PROP_OP, [{?PROP_ID, Id}]} ||
                            #?ACTIVATION{viewFsm = V,
                                         opRef = {?OP, Id}} <-
                               mnesia:table(?ACTIVATION),
                            V == ViewFsm])
                end,

  % Fold the ops query over each unique view, building the list of
  % structs.
  F = fun() ->
           qlc:fold(fun(ViewFsm, Acc) ->
                         OpStructs = do_query(OpStructsFn(ViewFsm)),
                         Node = atom_to_list(node(ViewFsm)),
                         Pid = pid_to_list(ViewFsm),
                         Attributes = case Cookie of
                                        ?ANY_COOKIE ->
                                          [{?PROP_NODE, Node},
                                           {?PROP_VIEW, Pid}];
                                        Cookie ->
                                          [{?PROP_NODE, Node},
                                           {?PROP_VIEW, Pid},
                                           {?PROP_COOKIE, Cookie}]
                                        end,
                         [{?PROP_ACTIVE_VIEW, Attributes, OpStructs} | Acc]
                    end, [], UniqueViewsFn())
      end,
  do_fun(F).

%% Executes the supplied function within a transaction and returns the result.
do_fun(F) ->
  {atomic, Result} = mnesia:transaction(F),
  Result.

%% Executes the supplied query handle and returns the result.
do_query(Q) ->
  F = fun() -> qlc:e(Q) end,
  do_fun(F).

%% ----------------------------------------------------------------------------
%% Unimplemented gen_server callbacks.
%% ----------------------------------------------------------------------------

handle_cast(_, _) -> throw(?REASON_NOT_IMPLEMENTED).
code_change(_, _, _) -> throw(?REASON_NOT_IMPLEMENTED).
